package cn.zzzhy.core.util;

import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.joda.time.DateTime;
import org.springframework.dao.DataAccessException;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * author:zzzhy
 * since :15/11/9.
 */
public class EntityClassWrapper {

    private static ConcurrentHashMap<Class<?>,EntityClassWrapper> cacheWrapper = new ConcurrentHashMap<Class<?>,EntityClassWrapper>();

    private Class<?> clazz;
    private List<Field> fields = Lists.newArrayList();
    private final LinkedHashMap<String, ColumnField> columnFields;
    private String tableName;
    private final ColumnField idColumnField;

    public <T> boolean isIdPresent(T entity) {
        return idColumnField != null  && idColumnField.get(entity) != null;
    }

    static class BadFieldException extends DataAccessException {
        public BadFieldException(String msg, Throwable cause) {
            super(msg, cause);
        }
    }

    static class DuplicateIdException extends DataAccessException {
        public DuplicateIdException(Class<?> entityClass) {
            super(entityClass.getName() + " has duplicate id annotation present !");
        }
    }

    static class NoAnnotationException extends DataAccessException {
        public NoAnnotationException(Class<?> entityClass, Class<Table> tableClass) {
            super(entityClass.getName() + " has no "+ tableClass.getName() + " annotation present !");
        }
    }

    public static abstract class BaseField {
        protected final Field field;

        public BaseField(Field field) {
            field.setAccessible(true);
            this.field = field;
        }

        public Field getField() {
            return field;
        }

        public String getName() {
            return field.getName();
        }

        public Class<?> getType() {
            return field.getType();
        }

        public Object get(Object entity) {
            try {
                return field.get(entity);
            } catch (Exception e) {
                throw new BadFieldException(e.getMessage(), e);
            }
        }

        public Object getJdbcValue(Object entity) {
            Object value = get(entity);
            if (value instanceof Enum) {
                value = value.toString();
            } else if (value instanceof DateTime) {
                value = ((DateTime) value).toDate();
            }
            return value;
        }

        public void set(Object entity, Object value) {
            try {
                field.set(entity, value);
            } catch (Exception e) {
                throw new BadFieldException(e.getMessage(), e);
            }
        }
    }

    public static class ColumnField extends BaseField {
        private Id id;
        private Column column;
        private String columnName;

        public ColumnField(Field field) {
            super(field);

            id = field.getAnnotation(Id.class);
            column = field.getAnnotation(Column.class);

            if (column != null && !Strings.isNullOrEmpty(column.name())) {
                columnName = column.name();
            } else {
                columnName = CaseFormat.LOWER_CAMEL.to(
                        CaseFormat.LOWER_UNDERSCORE, field.getName());
            }

        }

        public boolean isId() {
            return id != null;
        }

        public String getColumnName() {
            return columnName;
        }
    }

    public EntityClassWrapper(Class<?> entityClass) {
        if(!Reflects.isAnnotationPresent(entityClass,Table.class)){
            throw new NoAnnotationException(entityClass,Table.class);
        }
        this.clazz = entityClass;
        Reflects.ClassAnnotation<Table> table = Reflects.getAnnotation(entityClass,Table.class);
        assert table != null;
        tableName = table.getAnnotation().name();
        if(Strings.isNullOrEmpty(tableName)){
            tableName = entityClass.getSimpleName();
            tableName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,tableName);
        }
        scanFields(entityClass);
        columnFields = Maps.newLinkedHashMap();
        idColumnField = scanColumns();
    }

    private ColumnField scanColumns() {
        boolean idShowUp = false;
        ColumnField idField = null;
        for (Field field : fields) {
            Id id = field.getAnnotation(Id.class);
            Column column = field.getAnnotation(Column.class);
            if(id == null && column == null){
                continue;
            }
            if(id != null){
                if(idShowUp){
                    throw new DuplicateIdException(clazz);
                }
                idShowUp = true;
                idField = new ColumnField(field);
            }
            columnFields.put(field.getName(),new ColumnField(field));
        }
        return idField;
    }

    private void scanFields(Class<?> entityClass) {
        fields = Reflects.getFields(entityClass,false);
    }

    public static EntityClassWrapper wrap(Class<?> entityClass) {
        if(cacheWrapper.get(entityClass) == null){
            EntityClassWrapper wrapper = new EntityClassWrapper(entityClass);
            cacheWrapper.putIfAbsent(entityClass,wrapper);
            return wrapper;
        }else{
            return cacheWrapper.get(entityClass);
        }
    }

    public Class<?> getClazz() {
        return clazz;
    }

    public void setClazz(Class<?> clazz) {
        this.clazz = clazz;
    }

    public List<Field> getFields() {
        return fields;
    }

    public void setFields(List<Field> fields) {
        this.fields = fields;
    }

    public Collection<ColumnField> getColumnFields() {
        return columnFields.values();
    }

    public String getTableName() {
        return tableName;
    }

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public ColumnField getIdColumnField() {
        return idColumnField;
    }
}
